from xml.dom import minidom
import sys
import time
import os
import zipfile
import platform
import shutil
import atexit


SCRIPT_DIR = os.path.split(os.path.realpath(__file__))[0]
CONFIG_FILE_NAME = SCRIPT_DIR + '/config/backup_config.xml'
LOGFILE = SCRIPT_DIR + '/log.txt'

ret = -1
nolog = False


def cls():
    if platform.system() == 'Windows':
        os.system('cls')
    else:
        os.system('clear')


def show_beautiful_wheel(fps: int = 15, dur: float = 0.5, fade=True):
    wheel_seq = ['-', '\\', '|', '/']
    no_frames = int(fps * dur)
    for fr in range(no_frames):
        print(wheel_seq[fr % 4], end='')
        time.sleep(1.0/float(fps))
        print('\r', end='')
    print(' \r' if fade else'\n', end='')


def draw_line():
    for i in range(shutil.get_terminal_size().columns):
        print('-', end='')
    print('\n', end='')


def draw_block_line():
    for i in range(shutil.get_terminal_size().columns):
        print('█', end='')
    print('\n', end='')


def add_log(msg, end='\n', logfile: str = LOGFILE, show_time=True) -> int:
    if nolog:
        return -1

    logfile_dir = os.path.split(logfile)[0]

    if not os.path.exists(logfile):
        if not os.path.exists(logfile_dir):
            try:
                os.mkdir(logfile_dir)
            except FileNotFoundError:
                print('无法创建log文件目录')
                return 1
        try:
            logfile_fd = open(file=logfile, mode='w')
        except Exception as e:
            print('无法创建log文件')
            return 2
        if logfile_fd is not None:
            logfile_fd.write((('[' + time.asctime() + ']: ') if show_time else '') + 'LOG文件创建！' + end)
            print('log文件创建成功！')
            logfile_fd.close()

    try:
        logfile_fd = open(file=logfile, mode='a')
    except IOError:
        print('LOG文件写入失败！')
        return 3

    logfile_fd.write((('[' + time.asctime() + ']: ') if show_time else '') + msg + end)
    logfile_fd.close()
    return 0


def print_and_log(msg):
    print(msg)
    add_log(msg)


class BackupTask:
    def __init__(self, path_to_backup: list[str], target_archive: str, arcprefix: str):
        self.path_to_backup: list[str] = path_to_backup
        self.target_archive: str = target_archive
        self.arcprefix: str = arcprefix
    

def check_config_file() -> bool:
    r = True
    f = None
    try:
        f = open(CONFIG_FILE_NAME, mode='r')
    except FileNotFoundError:
        r = False
        print_and_log('配置文件不存在，需要创建')
    if f:
        f.close()
    return r


def read_config_file() -> minidom.Document:
    print_and_log('读取配置文件')
    return minidom.parse(CONFIG_FILE_NAME)


def create_config_file() -> bool:
    dom: minidom.Document = minidom.getDOMImplementation().createDocument(None, 'root', None)
    p: tuple = os.path.split(CONFIG_FILE_NAME)
    cfg_dir: str = p[0]
    if not os.path.exists(cfg_dir):
        try:
            os.mkdir(cfg_dir)
        except FileNotFoundError:
            print_and_log('无法创建配置文件目录！')
            return False
    with open(CONFIG_FILE_NAME, 'w') as f:
        if f:
            dom.writexml(f)
            f.close()
            print_and_log('配置文件创建成功！')
            return True
    print_and_log('配置文件创建失败。')
    return False


def construct_backup_tasks(root: minidom.Element) -> list[BackupTask]:
    task_list: list[BackupTask] = []
    task_node_list: list[minidom.Element] = root.getElementsByTagName('task')
    for task_node in task_node_list:
        if task_node.hasAttribute('target'):
            target_file: str = task_node.getAttribute('target')
            file_nodes: list[minidom.Element] = task_node.getElementsByTagName('path')
            flist: list[str] = []
            for file_node in file_nodes:
                flist.append(file_node.childNodes[0].data)
            arcprefix: str = task_node.getAttribute('arcprefix')
            task: BackupTask = BackupTask(flist, target_file, arcprefix)
            task_list.append(task)
    return task_list


def user_display_task_list(task_list: list[BackupTask]) -> list[BackupTask]:
    cnt = 1
    print('任务列表')
    draw_line()
    for task in task_list:
        print("任务%d\n目标文件：%s\n待备份文件：" % (cnt, task.target_archive))
        for ptb in task.path_to_backup:
            print(ptb)
        draw_line()
        cnt += 1
    return task_list


def user_create_task(task_list: list[BackupTask]) -> list[BackupTask]:
    print('输入备份目标文件：')
    target_file: str = input()
    print('输入待备份文件（用半角分号分隔）：')
    ptbs: list[str] = input().split(';')
    print('请输入前缀（待备份文件在压缩包中的所处的文件夹，没有直接按Enter）：')
    arch_prefix: str = input()
    task: BackupTask = BackupTask(ptbs, target_file, arch_prefix)
    task_list.append(task)
    print('操作完成！')
    return task_list


def user_modify_task(task_list: list[BackupTask]) -> list[BackupTask]:
    print('请输入要修改的任务号（从1开始，当前任务数量为%d）' % len(task_list))
    task_idx = int(input())
    if 0 < task_idx <= len(task_list):
        print('输入备份任务的目标压缩文件，你需要备份的文件会被存在这个压缩文件中（zip格式）：')
        target_file = input()
        print('输入你需要备份的文件（用半角分号分隔）：')
        ftbs = input().split(';')
        print('请输入前缀（待备份文件在压缩包中的所处的文件夹，没有直接按Enter）：')
        arch_prefix = input()
        task = BackupTask(ftbs, target_file, arch_prefix)
        task_list[task_idx-1] = task
        print('操作完成！')
    else:
        print('任务号过大或小于0')
    return task_list


def user_remove_task(task_list: list[BackupTask]) -> list[BackupTask]:
    print('请输入要删除的任务号（从1开始，当前任务数量为%d）' % len(task_list))
    task_idx = int(input())
    if 0 < task_idx <= len(task_list):
        task_list.pop(task_idx-1)
    else:
        print('任务号过大或小于0')
    return task_list


def set_file_time(filename, update_time='now', access_time='now'):
    filename = os.path.abspath(filename)
    if update_time == 'now':
        new_update_time = time.time()
    else:
        new_update_time = time.mktime(time.strptime(update_time, "%Y-%m-%d %H:%M:%S"))
    if access_time == 'now':
        new_access_time = time.time()
    else:
        new_access_time = time.mktime(time.strptime(access_time, "%Y-%m-%d %H:%M:%S"))
    os.utime(filename, (new_access_time, new_update_time))


def user_run_backup_task(task_list: list[BackupTask], root: minidom.Element,
                         fast_archive: bool = False) -> minidom.Element:
    print('压缩备份任务开始！')
    add_log('压缩备份任务开始！')

    root = save_modifications(task_list)
    cnt = 1

    if fast_archive:
        compress_type = zipfile.ZIP_STORED
    else:
        compress_type = zipfile.ZIP_DEFLATED

    for task in task_list:
        print("执行中……（任务%d/%d）" % (cnt, len(task_list)))
        add_log("执行中……（任务%d/%d）" % (cnt, len(task_list)))

        target_dir = os.path.split(task.target_archive)[0]
        if not (os.path.exists(target_dir) and os.path.isdir(target_dir)):
            try:
                os.mkdir(target_dir)
            except Exception as e:
                print('无法创建目标压缩文件的目录，略过任务！')
                add_log('无法创建目标压缩文件的目录，略过任务！')
                continue
        zf: zipfile.ZipFile = zipfile.ZipFile(task.target_archive, mode='w')
        for fpath in task.path_to_backup:
            if os.path.exists(fpath):
                if os.path.isfile(fpath):
                    try:
                        # print_and_log('压缩文件【%s】' % fpath)
                        zf.write(fpath, compress_type=compress_type,
                                 arcname=task.arcprefix + os.path.split(os.path.realpath(fpath))[1])
                    except ValueError:
                        set_file_time(fpath)
                        print_and_log('文件【%s】疑似时间戳不符合要求（小于1980），已更改时间戳' % fpath)
                        zf.write(fpath, compress_type=compress_type,
                                 arcname=task.arcprefix + os.path.split(os.path.realpath(fpath))[1])
                    except:
                        print('未知错误')
                        add_log('未知错误')
                else:
                    archive_folder(zf, fpath, task.arcprefix, fast_archive)
            else:
                print('文件或文件夹：【%s】不存在！正在略过' % fpath)
                add_log('文件或文件夹：【%s】不存在！正在略过' % fpath)
        zf.close()
        cnt += 1
    print('压缩备份完成！')
    add_log('压缩备份完成')
    return root


def archive_folder(zf, fpath, arcprefix, fast_archive=False):
    # print_and_log('正在压缩文件夹【%s】' % fpath)
    children = os.listdir(fpath)

    if fast_archive:
        compress_type = zipfile.ZIP_STORED
    else:
        compress_type = zipfile.ZIP_DEFLATED

    for child in children:
        child_full_path = os.path.join(fpath, child)
        child_arc_path = os.path.join(arcprefix, child)
        if os.path.isfile(child_full_path):
            try:
                # print_and_log('正在压缩文件【%s】' % child_full_path)
                zf.write(child_full_path, arcname=child_arc_path, compress_type=compress_type)
            except ValueError:
                print_and_log('文件【%s】疑似时间戳不符合要求（小于1980），已更改时间戳' % child_full_path)
                set_file_time(child_full_path)
                zf.write(child_full_path, arcname=child_arc_path, compress_type=compress_type)
        else:
            zf.write(child_full_path, arcname=child_arc_path, compress_type=compress_type)
            archive_folder(zf, child_full_path, child_arc_path, fast_archive)


def save_modifications(task_list: list[BackupTask]) -> minidom.Element:
    print('转化配置数据为XML格式……')
    add_log('转化配置数据为XML格式……')
    dom = minidom.Document()
    root = dom.createElement('root')
    for task in task_list:
        task_node = dom.createElement('task')
        task_node.setAttribute('target', task.target_archive)
        for ftb in task.path_to_backup:
            file_node = dom.createElement('path')
            file_text = dom.createTextNode(ftb)
            file_node.appendChild(file_text)
            task_node.appendChild(file_node)
        task_node.setAttribute('arcprefix', task.arcprefix)
        root.appendChild(task_node)
    dom.appendChild(root)
    print('数据成功转化为XML格式！正在保存到文件……')
    add_log('数据成功转化为XML格式！正在保存到文件……')
    with open(CONFIG_FILE_NAME, 'w', encoding='utf-8') as f:
        dom.writexml(f, addindent='  ', newl='\n', encoding='utf-8')
    return root


def main() -> int:
    global nolog
    if_fast_archive = False
    if '--nolog' in sys.argv:
        nolog = True
        print('检测到参数“--nolog”，日志功能已关闭')
    if '--fast' in sys.argv:
        add_log('检测到参数--fast，将进行STORE方式归档')
        if_fast_archive = True

    if not check_config_file():
        if not create_config_file():
            print_and_log('创建文件失败！')
            return 1
        else:
            dom = minidom.Document()
            root = minidom.Element('root')
            dom.appendChild(root)
    else:
        dom = read_config_file()
        if dom is None:
            print_and_log('DOM：实例化失败！')
            return 1

    root = dom.documentElement
    task_list = construct_backup_tasks(root)

    if '--auto' in sys.argv:
        add_log('检测到参数--auto，开始自动备份任务')
        print('\n正  在  自  动  执  行  备  份  脚  本  ')
        user_run_backup_task(task_list, root, if_fast_archive)
        print('自动备份已完成，5秒后退出！')
        add_log('自动备份完成')
        show_beautiful_wheel(20, 5)
        return 0

    time.sleep(0.5)

    while True:
        cls()
        show_beautiful_wheel()
        draw_block_line()
        print('')
        print('》  欢  迎  使  用  备  份  脚  本  《')
        print('')
        draw_block_line()
        print('')
        print('1、查看任务\n2、创建/更改/删除任务\n3、立即备份\n4、退出并保存配置文件更改\n5、退出，不保存配置文件更改\n请输入指令并按下Enter！')
        add_log('用户进入备份脚本主界面')
        cmd = input()
        if cmd == '1':
            cls()
            show_beautiful_wheel()
            add_log('用户进入功能1：查看任务')
            user_display_task_list(task_list)
            print('输入任意指令继续！')
            input()
        elif cmd == '2':
            cls()
            show_beautiful_wheel()
            add_log('用户进入功能2：创建/更改/删除任务')
            while True:
                print('请选择指令')
                draw_line()
                print('1、创建任务\n2、更改任务\n3、删除任务\n4、退出\n请输入指令并按下Enter！')
                cmd = input()
                if cmd == '1':
                    add_log('用户进入功能2.1：创建任务')
                    cls()
                    show_beautiful_wheel()
                    user_create_task(task_list)
                elif cmd == '2':
                    cls()
                    show_beautiful_wheel()
                    add_log('用户进入功能2.2：更改任务')
                    user_modify_task(task_list)
                elif cmd == '3':
                    cls()
                    show_beautiful_wheel()
                    add_log('用户进入功能2.3：删除任务')
                    user_remove_task(task_list)
                elif cmd == '4':
                    add_log('用户手动退出功能2')
                    break
                else:
                    pass
        elif cmd == '3':
            cls()
            show_beautiful_wheel()
            add_log('用户进入功能3：立刻备份')
            root = user_run_backup_task(task_list, root, if_fast_archive)
        elif cmd == '4':
            add_log('用户进入功能4：退出并保存配置文件更改')
            save_modifications(task_list)
            break
        elif cmd == '5':
            add_log('用户进入功能5：退出但不保存配置文件更改')
            break
        else:
            pass
    show_beautiful_wheel()
    print("欢迎下次使用，输入任意指令继续退出！")
    return 0


def handle_exit():
    if ret != 0:
        add_log('程序意外退出！')


if __name__ == '__main__':
    atexit.register(handle_exit)
    ret = main()
    add_log('程序退出，返回码：%d' % ret)
    exit(ret)
